<?php
/**
 * @author 595949289@qq.com
 * @datetime 2022/9/17 15:01
 */

namespace CuiFox\AliOss;

use Yii;
use OSS\OssClient;
use yii\base\Component;
use yii\helpers\FileHelper;
use yii\imagine\Image;
use yii\web\ForbiddenHttpException;
use yii\web\NotFoundHttpException;
use yii\web\Request;
use yii\web\Response;

class Server extends Component
{
    /**
     * @var array Buckets
     * [
     *  [
     * 'name' => '***',
     * 'private' => true,
     * 'accessKeyId' => '***',
     * 'accessKeySecret' => '***',
     * ],
     * ]
     */
    public $buckets;
    /**
     * @var array bucket
     * [
     * 'name' => '***',
     * 'private' => true,
     * 'accessKeyId' => '***',
     * 'accessKeySecret' => '***',
     * ]
     */
    private $bucket;
    /**
     * @var string Root
     */
    public $storage = 'storage/oss';
    /**
     * @var Request
     */
    private $request;
    /**
     * @var Response
     */
    private $response;
    /**
     * @var string Path
     */
    private $path;
    /**
     * @var $_FILES
     */
    private $file;

    /**
     * @var string key
     */
    private $key;

    private $pSize;

    /**
     * Init
     * @throws NotFoundHttpException
     */
    public function init()
    {
        $this->request = Yii::$app->request;
        $this->response = Yii::$app->response;

        // 判断是否为ip
        if ($this->isIPFormat($this->request->getHostName())) {
            $this->setBucketByPort();
        } else {
            $this->setBucketByDomain();
        }
    }

    /**
     * @throws ForbiddenHttpException
     * @throws \yii\base\ErrorException
     * @throws \yii\base\Exception
     */
    public function handle()
    {
        $method = $this->request->getMethod();
        switch ($method) {
            case 'DELETE':
                $this->delete();
                break;
            case 'PUT':
                $this->put();
                break;
            case 'POST':
                $this->post();
                break;
            case 'GET':
                $this->get();
                break;
            case 'HEAD':
                $this->head();
                break;
        }
    }

    /**
     * @param $name
     * @throws NotFoundHttpException
     */
    public function setBucket($name)
    {
        $buckets = array_column($this->buckets, null, 'name');
        if (!isset($buckets[$name])) {
            throw new NotFoundHttpException('Bucket not Found');
        }
        $this->bucket = $buckets[$name];
    }

    /**
     * 通过域名设置桶
     * @throws NotFoundHttpException
     */
    public function setBucketByDomain()
    {
        preg_match("#(.*?)\.#i", $this->request->getHostName(), $match);
        $this->setBucket($match[1]);
    }

    /**
     * 通过ip设置桶
     * @throws NotFoundHttpException
     */
    public function setBucketByPort()
    {
        $port = $this->request->getPort();
        $names = array_column($this->buckets, 'name', 'port');
        $this->setBucket(isset($names[$port]) ? $names[$port] : '');
    }

    /**
     * 通过accessKeyId设置桶
     * @param $accessKeyId
     * @throws NotFoundHttpException
     */
    public function setBucketByAccessKeyId($accessKeyId)
    {
        $names = array_column($this->buckets, 'name', 'accessKeyId');
        $this->setBucket(isset($names[$accessKeyId]) ? $names[$accessKeyId] : '');
    }

    /**
     * Set Path
     * @param $path
     */
    public function setPath($path)
    {
        $this->path = $path;
    }

    /**
     * Get Path
     * @return mixed
     */
    private function getPath()
    {
        $path = $this->getBucketPath() . '/' . ltrim($this->path, '/');
        return str_replace(['/', '\\'], [DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR], $path);
    }

    /**
     * Get bucket path
     * @return string
     */
    private function getBucketPath()
    {
        return Yii::getAlias('@app/' . $this->storage . '/' . $this->bucket['name']);
    }

    /**
     * Delete
     * @throws ForbiddenHttpException
     * @throws \yii\base\ErrorException
     * @throws \yii\base\Exception
     */
    private function delete()
    {
        if (!$this->verifyPut()) {
            throw new ForbiddenHttpException;
        }

        $path = FileHelper::createDirectory($this->getPath());
        if (is_file($path)) {
            FileHelper::unlink($path);
        } else {
            FileHelper::removeDirectory($path);
        }
    }

    /**
     * Put
     * @throws ForbiddenHttpException
     * @throws \yii\base\Exception
     */
    private function put()
    {
        if (!$this->verifyPut()) {
            throw new ForbiddenHttpException;
        }

        FileHelper::createDirectory(dirname($this->getPath()));
        $fp = fopen($this->getPath(), "w");
        $input = fopen("php://input", "r");
        while ($data = fread($input, 1024)) {
            fwrite($fp, $data);
        }
        fclose($fp);
        fclose($input);
    }

    /**
     * @throws ForbiddenHttpException
     * @throws \Exception
     */
    private function post()
    {
        $verify = $this->verifyPost();
        if (!$verify) {
            throw new ForbiddenHttpException();
        }

        $chunk = $this->request->post('chunk', 0);
        $chunks = $this->request->post('chunks', 1);

        $path = $this->getBucketPath() . DIRECTORY_SEPARATOR . $this->key;
        $info = pathinfo($path);
        $upload = new Uploader($this->file['tmp_name'], $chunk + 1, $chunks, $info['basename'], dirname($path));
        $upload->handle();
    }

    /**
     * @throws \yii\base\InvalidConfigException
     */
    private function pSize()
    {
        $this->path = $this->request->getPathInfo();
        if (strpos($this->path, '!') !== false) {
            $info = explode('!', $this->path);
            $this->path = $info[0];
            $this->pSize = $info[1];
        }
    }

    /**
     * Get
     * @throws ForbiddenHttpException
     * @throws NotFoundHttpException
     * @throws \yii\base\Exception
     * @throws \yii\base\InvalidConfigException
     */
    private function get()
    {
        $this->pSize();
        $path = $this->getPath();
        if (!is_file($path)) {
            throw new NotFoundHttpException();
        }

        if ($this->bucket['private'] && !$this->verifyGet()) {
            throw new ForbiddenHttpException('Private bucket');
        }

        if ($this->pSize) {
            $size = str_replace('p', '', $this->pSize);
            if (!in_array($size, $this->bucket['size'])) {
                throw new NotFoundHttpException();
            }

            $origin = $this->getPath();
            list($width, $height) = getimagesize($origin);
            $scale = $width / $height;

            if ($scale > 0) {
                $width = $size;
                $height = $size / $scale;
            } else {
                $width = $size * $scale;
                $height = $size;
            }

            $this->path = $this->pSize . '/' . $this->path;
            $path = $this->getPath();
            if (!is_file($path)) {
                FileHelper::createDirectory(dirname($path));
                Image::thumbnail($origin, $width, $height)->save($path, ['quality' => 90]);
            }
        }

        $this->response->getHeaders()
            ->set('Pragma', 'public')
            ->set('Expires', '0')
            ->set('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
            ->set('Content-Transfer-Encoding', 'binary');
        $this->response->sendFile($path, null, ['inline' => true])->send();
    }

    /**
     * Verify Get
     * @return bool
     * @throws ForbiddenHttpException
     */
    private function verifyGet()
    {
        $private = isset($this->bucket['private']) ? $this->bucket['private'] : true;
        if (!$private) {
            return true;
        }

        $accessKeyId = $this->request->get('OSSAccessKeyId');
        $expires = $this->request->get('Expires');
        $signature = $this->request->get('Signature');


        if ($this->bucket['accessKeyId'] != $accessKeyId) {
            throw new ForbiddenHttpException();
        }

        if ($expires < time()) {
            throw new ForbiddenHttpException('Expires expired');
        }

        $data = [
            'method' => $this->request->getMethod(),
            'content-md5' => '',
            'content-type' => '',
            'date' => $expires,
            'path' => rawurldecode('/' . $this->bucket['name'] . '/' . $this->path) . urldecode($this->getSignQueryString()),
        ];

        $string = implode("\n", $data);
        $encode = base64_encode(hash_hmac('sha1', $string, $this->bucket['accessKeySecret'], true));
        return $encode == $signature;
    }

    /**
     * @return string
     */
    private function getSignQueryString()
    {
        $options = $this->request->get();
        $signAbleQueryStringParams = [];
        $signAbleList = array(
            OssClient::OSS_PART_NUM,
            'response-content-type',
            'response-content-language',
            'response-cache-control',
            'response-content-encoding',
            'response-expires',
            'response-content-disposition',
            OssClient::OSS_UPLOAD_ID,
            OssClient::OSS_COMP,
            OssClient::OSS_LIVE_CHANNEL_STATUS,
            OssClient::OSS_LIVE_CHANNEL_START_TIME,
            OssClient::OSS_LIVE_CHANNEL_END_TIME,
            OssClient::OSS_PROCESS,
            OssClient::OSS_POSITION,
            OssClient::OSS_SYMLINK,
            OssClient::OSS_RESTORE,
        );

        foreach ($signAbleList as $item) {
            if (isset($options[$item])) {
                $signAbleQueryStringParams[$item] = $options[$item];
            }
        }
        return '?' . http_build_query($signAbleQueryStringParams);
    }

    /**
     * Head
     * @throws NotFoundHttpException
     */
    private function head()
    {
        $path = $this->getPath();
        if (!is_file($path)) {
            throw new NotFoundHttpException();
        }
    }

    /**
     * @return array
     */
    private function getHeaders()
    {
        $data = [];
        $headers = $this->request->getHeaders()->toArray();
        foreach ($headers as $key => $it) {
            $data[$key] = isset($it[0]) ? $it[0] : '';
        }
        unset($headers);
        return $data;
    }

    /**
     * Verify put
     * @return bool
     * @throws \yii\base\InvalidConfigException
     */
    private function verifyPut()
    {
        $this->path = $this->request->getPathInfo();
        $headers = $this->getHeaders();

        $keys = [
            OssClient::OSS_CONTENT_MD5,
            OssClient::OSS_CONTENT_TYPE,
            OssClient::OSS_DATE,
        ];
        $string = $this->request->getMethod() . "\n";
        foreach ($keys as $it) {
            $it = strtolower($it);
            $string .= isset($headers[$it]) ? $headers[$it] : '';
            $string .= "\n";
        }

        foreach ($headers as $key => $value) {
            if (substr(strtolower($key), 0, 6) === OssClient::OSS_DEFAULT_PREFIX) {
                $string .= strtolower($key) . ':' . $value . "\n";
            }
        }
        $string .= urldecode('/' . $this->bucket['name'] . '/' . $this->path);
        $sign = 'OSS ' . $this->bucket['accessKeyId'] . ':' . base64_encode(hash_hmac('sha1', $string,
                $this->bucket['accessKeySecret'], true));

        return $sign == $headers[strtolower(OssClient::OSS_AUTHORIZATION)];
    }

    /**
     * @throws ForbiddenHttpException
     * @throws NotFoundHttpException
     */
    private function verifyPost()
    {
        $policy = $this->request->post('policy');
        $signature = $this->request->post('signature');
        $key = $this->request->post('key');

        if (!$key) {
            throw new ForbiddenHttpException('Key cannot be empty');
        }

        if (!isset($_FILES['file'])) {
            throw new ForbiddenHttpException('File cannot be empty');
        }

        $this->file = $_FILES['file'];
        $this->key = $key;

        if (!$policy) {
            throw new ForbiddenHttpException('Policy cannot be empty');
        }

        if (!$signature) {
            throw new ForbiddenHttpException('Signature cannot be empty');
        }

        $encode = base64_encode(hash_hmac('sha1', $policy, $this->bucket['accessKeySecret'], true)); //签名算法
        if ($encode != $signature) {
            throw new ForbiddenHttpException('Signature Does Not Match');
        }

        $policy = json_decode(base64_decode($policy), true);

        if (isset($policy['expiration'])) {
            $expiration = strtotime(str_replace(['T', 'Z'], ' ', $policy['expiration']));
            if ($expiration < time()) {
                throw new ForbiddenHttpException('Expiration expired');
            }
        }

        foreach ($policy['conditions'] as $key => $it) {
            if ($key == 0) {
                if (isset($it['bucket']) && $it['bucket']) {
                    $this->setBucket($it['bucket']);
                } else {
                    throw new ForbiddenHttpException('Conditions bucket cannot be empty');
                }
            } else {
                if ($it[0] == 'content-length-range') {
                    if (!($this->file['size'] >= $it[1] && $this->file['size'] <= $it[2])) {
                        throw new ForbiddenHttpException('Conditions file size out of range');
                    }
                } elseif ($it[0] == 'starts-with') {
                    if (strpos($this->key, $it[2]) !== 0) {
                        throw new ForbiddenHttpException('Conditions starts with error');
                    }
                }
            }
        }

        return true;
    }

    /**
     * Check if the endpoint is in the IPv4 format, such as xxx.xxx.xxx.xxx:port or xxx.xxx.xxx.xxx.
     *
     * @param string $endpoint The endpoint to check.
     * @return boolean
     */
    public function isIPFormat($endpoint)
    {
        $ip_array = explode(":", $endpoint);
        $hostname = $ip_array[0];
        $ret = filter_var($hostname, FILTER_VALIDATE_IP);
        if (!$ret) {
            return false;
        } else {
            return true;
        }
    }
}